/*
 * XSplitPane.java
 * 
 * Copyright (c) 2006 - 2007
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.xfc.components;

import java.awt.Component;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;

import javax.swing.BorderFactory;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.SwingConstants;

/**
 * A split pane component - similar to JSplitPane. This has been created to fix some
 * deficiencies in JSplitPane. Specifically:
 * <ul
 *   <li>JSplitPanes do not nest well. They do not draw correctly at their intersections.
 *   <li>Disabling a JSplitPane (so it can't be resized) disables all JSplitPanes. 
 *   <li>JSplitPanes draw borders even through they have do idea where they will be embedded. Just removing
 *     the border on a JSplitPane can cause certain l+fs to render incorrectly. It's better that split panes
 *     don't have borders by default.
 * </ul>
 * 
 * XSplitPane has some additional features for managing / creating nested split panes.
 * 
 * @author Devon Carew
 */
public class XSplitPane
	extends JPanel
{
	public static final int VERTICAL 			= SwingConstants.VERTICAL;
	public static final int HORIZONTAL 			= SwingConstants.HORIZONTAL;
	
	public static final int	DIVIDER_WIDTH 		= 4;
	
	private JComponent 			leftComponent;
	private JComponent 			rightComponent;
	
	private DividerComponent 	divider;
	
	private int 				orientation = HORIZONTAL;
	
	private boolean				requiresCleanLayout;
	
	public XSplitPane()
	{
		divider = new DividerComponent();
		divider.setVisible(false);
		
		super.add(divider);
	}
	
	public XSplitPane(int orientation)
	{
		this();
		
		setOrientation(orientation);
	}

	public XSplitPane(int orientation, JComponent leftComponent, JComponent rightComponent)
	{
		this(orientation);
		
		setLeftComponent(leftComponent);
		setRightComponent(rightComponent);
	}
	
	public void createDefaultBorder()
	{
		setBorder(BorderFactory.createEmptyBorder(DIVIDER_WIDTH, DIVIDER_WIDTH, DIVIDER_WIDTH, DIVIDER_WIDTH));
	}
	
	public Component add(Component comp)
	{
		throw new IllegalStateException("You cannot call add() directly for a split pane");
	}
	
	public void remove(Component comp)
	{
		throw new IllegalStateException("You cannot call remove() directly for a split pane");
	}
	
	public void removeAll()
	{
		throw new IllegalStateException("You cannot call removeAll() directly for a split pane");
	}

	public JComponent getLeftComponent()
	{
		return leftComponent;
	}
	
	public void setLeftComponent(JComponent leftComponent)
	{
		if (this.leftComponent != null)
			super.remove(this.leftComponent);
		
		this.leftComponent = leftComponent;
		
		if (this.leftComponent != null)
			super.add(this.leftComponent);
		
		update();
	}
	
	public JComponent getRightComponent()
	{
		return rightComponent;
	}
	
	public void setRightComponent(JComponent rightComponent)
	{
		if (this.rightComponent != null)
			super.remove(this.rightComponent);
		
		this.rightComponent = rightComponent;
		
		if (this.rightComponent != null)
			super.add(this.rightComponent);
		
		update();
	}
	
	public int getOrientation()
	{
		return orientation;
	}
	
	public void setOrientation(int orientation)
	{
		if (orientation != HORIZONTAL && orientation != VERTICAL)
			throw new IllegalArgumentException("orientation must be either HORIZONTAL or VERTICAL");
		
		this.orientation = orientation;
		
		update();
	}
	
	public void splitLeftComponent(int orientation)
	{
		splitLeftComponent(orientation, null);
	}
	
	public void splitLeftComponent(int orientation, JComponent rightComponent)
	{
		JComponent oldLeft = getLeftComponent();
		
		setLeftComponent(null);
		
		setLeftComponent(new XSplitPane(orientation, oldLeft, rightComponent));
	}
	
	public void splitRightComponent(int orientation)
	{
		splitRightComponent(orientation, null);
	}
	
	public void splitRightComponent(int orientation, JComponent rightComponent)
	{
		JComponent oldRight = getRightComponent();
		
		setRightComponent(null);
		
		setRightComponent(new XSplitPane(orientation, oldRight, rightComponent));
	}
	
	public void setEnabled(boolean enabled)
	{
		super.setEnabled(enabled);
		
		divider.update();
	}

	public void setDividerLocation(int location)
	{
		if (leftComponent == null || rightComponent == null)
			return;
		
		if (requiresCleanLayout)
			doLayout();
		
		Insets insets = getInsets();
		
		int x = insets.left;
		int y = insets.top;
		int width = getWidth() - insets.left - insets.right;
		int height = getHeight() - insets.top - insets.bottom;
		
		if (getOrientation() == HORIZONTAL)
		{
			int minLocation = insets.left + leftComponent.getMinimumSize().width;
			int maxLocation = getWidth() - insets.right - rightComponent.getMinimumSize().width - DIVIDER_WIDTH;
			
			if (location > maxLocation) location = maxLocation;
			if (location < minLocation) location = minLocation;
			
			//if (location >= minLocation && location <= maxLocation && location != getDividerLocation())
			{
				// Resize -
				leftComponent.setBounds(x, y, location, height);
				divider.setBounds(x + location, y, DIVIDER_WIDTH, height);
				rightComponent.setBounds(x + location + DIVIDER_WIDTH, y,
					width - DIVIDER_WIDTH - location, height);
				
				leftComponent.validate();
				rightComponent.validate();
			}
		}
		else
		{
			int minLocation = insets.top + leftComponent.getMinimumSize().height;
			int maxLocation = getHeight() - (rightComponent.getMinimumSize().height + insets.bottom + insets.top);
			
			location = Math.min(Math.max(location, minLocation), maxLocation);
			
			//if (location >= minLocation && location <= maxLocation && location != getDividerLocation())
			{
				// Resize -
				leftComponent.setBounds(x, y, width, location);
				divider.setBounds(x, y + location, width, DIVIDER_WIDTH);
				rightComponent.setBounds(x, y + location + DIVIDER_WIDTH,
					width, height - DIVIDER_WIDTH - location);
				
				leftComponent.validate();
				rightComponent.validate();
			}
		}
	}
	
	public int getDividerLocation()
	{
		if (!divider.isVisible())
			return -1;
		
		if (getOrientation() == HORIZONTAL)
			return divider.getX();
		else
			return divider.getY();
	}
	
	/**
	 * Over-ridden because XSplitPane manages its own layout.
	 */
	public void doLayout()
	{
		if (getComponentCount() > 3)
			throw new IllegalStateException("there should not be more then 2 child components");
		
		if (leftComponent == null && rightComponent == null)
			return;
		
		Dimension 	size = getSize();
		Insets 		insets = getInsets();
		
		int x = insets.left;
		int y = insets.top;
		int width = size.width - insets.left - insets.right;
		int height = size.height - insets.top - insets.bottom;
		
		if (leftComponent != null && rightComponent == null)
		{
			leftComponent.setBounds(x, y, width, height);
		}
		else if (rightComponent != null && leftComponent == null)
		{
			rightComponent.setBounds(x, y, width, height);
		}
		else
		{
			if (requiresCleanLayout)
			{
				if (orientation == HORIZONTAL)
				{
					// HORIZONTAL
					int xPos = insets.left;
					int minLeft = leftComponent.getMinimumSize().width;
					
					leftComponent.setBounds(x, y, minLeft, height);
					xPos += minLeft;
					divider.setBounds(xPos, y, DIVIDER_WIDTH, height);
					xPos += DIVIDER_WIDTH;
					rightComponent.setBounds(xPos, y, rightComponent.getMinimumSize().width, height);
				}
				else
				{
					// VERTICAL
					int yPos = insets.top;
					int minLeft = leftComponent.getMinimumSize().height;
					
					leftComponent.setBounds(x, yPos, width, minLeft);
					yPos += minLeft;
					divider.setBounds(x, yPos, width, DIVIDER_WIDTH);
					yPos += DIVIDER_WIDTH;
					rightComponent.setBounds(x, yPos, width, rightComponent.getMinimumSize().height);
				}
				
				requiresCleanLayout = false;
			}
			
			// Distribute the extra space.
			
			if (orientation == HORIZONTAL)
			{
				int width1 = leftComponent.getWidth();
				int width2 = rightComponent.getWidth();
				
//				int min1 = leftComponent.getMinimumSize().width;
//				int min2 = rightComponent.getMinimumSize().width;
				
				int slop = width - (rightComponent.getX() + width2);
				
				// Calculate the weights.
				double leftWeight = calcWeight(leftComponent);
				double rightWeight = calcWeight(rightComponent);
				
				int leftSlop = (int)(slop * (leftWeight / (leftWeight + rightWeight + 0.00001d)));
				int rightSlop = slop - leftSlop;
				
				// TODO: we need to respect components min sizes better when we're resizing -
				
				
//				if (rightSlop + width2 < min2)
//				{
//					rightSlop = 
//					
//				}
				
				// Resize the components.
				leftComponent.setBounds(x, y, width1 + leftSlop, height);
				divider.setBounds(divider.getX() + leftSlop, y, DIVIDER_WIDTH, height);
				rightComponent.setBounds(rightComponent.getX() + leftSlop, y, width2 + rightSlop, height);
			}
			else
			{
				// VERTICAL
				
				int height1 = leftComponent.getHeight();
				int height2 = rightComponent.getHeight();
				
//				int min1 = leftComponent.getMinimumSize().height;
//				int min2 = rightComponent.getMinimumSize().height;
				
				int slop = height - (rightComponent.getY() + height2);
				
				// Calculate the weights.
				double leftWeight = calcWeight(leftComponent);
				double rightWeight = calcWeight(rightComponent);
				
				int leftSlop = (int)(slop * (leftWeight / (leftWeight + rightWeight + 0.00001d)));
				int rightSlop = slop - leftSlop;
				
//				if (slop < 0)
//				{
//					// Ensure we don't make either component too small.
//					if (rightSlop + height2 < min2)
//					{
//						int diff = height2 - min2;
//						rightSlop += diff;
//						leftSlop -= diff;
//					}
//					
//					if (leftSlop + height1 < min1)
//					{
//						int diff = height1 - min1;
//						leftSlop += diff;
//						rightSlop -= diff;
//					}
//				}
				
				// Resize the components.
				leftComponent.setBounds(x, y, width, height1 + leftSlop);
				divider.setBounds(x, divider.getY() + leftSlop, width, DIVIDER_WIDTH);
				rightComponent.setBounds(x, rightComponent.getY() + leftSlop, width, height2 + rightSlop);
			}
			
			leftComponent.validate();
			rightComponent.validate();
		}
	}
	
	private double calcWeight(JComponent component)
	{
		// TODO: also take into account weights assigned to the component contraints?
		// TODO: to the columns themselves?
		
		if (getOrientation() == HORIZONTAL)
		{
			return component.getPreferredSize().width / (double)getWidth();
		}
		else
		{
			return component.getPreferredSize().height / (double)getHeight();
		}
	}
	
	public Dimension getMinimumSize()
	{
		Dimension min = new Dimension();
		
		if (leftComponent != null)
			addToSizeCalc(min, leftComponent.getMinimumSize());
		if (rightComponent != null)
			addToSizeCalc(min, rightComponent.getMinimumSize());
		if (leftComponent != null && rightComponent != null)
			addToSizeCalc(min, divider.getPreferredSize());
		
		Insets insets = getInsets();
		
		min.width += insets.left + insets.right;
		min.height += insets.top + insets.bottom;
		
		return min;
	}
	
	public Dimension getPreferredSize()
	{
		Dimension pref = new Dimension();
		
		if (leftComponent != null)
			addToSizeCalc(pref, leftComponent.getPreferredSize());
		if (rightComponent != null)
			addToSizeCalc(pref, rightComponent.getPreferredSize());
		if (leftComponent != null && rightComponent != null)
			addToSizeCalc(pref, divider.getPreferredSize());
		
		Insets insets = getInsets();
		
		pref.width += insets.left + insets.right;
		pref.height += insets.top + insets.bottom;
		
		return pref;
	}
	
	private void addToSizeCalc(Dimension calc, Dimension newSize)
	{
		if (getOrientation() == HORIZONTAL)
		{
			calc.width += newSize.width;
			calc.height = Math.max(calc.height, newSize.height);
		}
		else
		{
			calc.width = Math.max(calc.width, newSize.width);
			calc.height += newSize.height;
		}
	}
	
	private void update()
	{
		requiresCleanLayout = true;
		
		divider.setVisible(leftComponent != null && rightComponent != null);
		
		if (divider.isVisible())
			divider.update();
	}
	
	private class DividerComponent
		extends JComponent
	{
		public DividerComponent()
		{
			setOpaque(false);
			
			addMouseMotionListener(new MouseMotionAdapter() {
				public void mouseDragged(MouseEvent event) {
					if (!XSplitPane.this.isEnabled())
						return;
					
					if (getOrientation() == HORIZONTAL)
						setDividerLocation(getX() + event.getX());
					else
						setDividerLocation(getY() + event.getY());
				}
			});
		}
		
		private void update()
		{
			if (!XSplitPane.this.isEnabled())
				setCursor(Cursor.getDefaultCursor());
			else if (getOrientation() == HORIZONTAL)
				setCursor(Cursor.getPredefinedCursor(Cursor.W_RESIZE_CURSOR));
			else
				setCursor(Cursor.getPredefinedCursor(Cursor.N_RESIZE_CURSOR));
		}
		
		public Dimension getPreferredSize()
		{
			return new Dimension(DIVIDER_WIDTH, DIVIDER_WIDTH);
		}
	}

}
